﻿/*
 * Copyright 2014 Twitter, Inc
 * This file is a derivative work modified by Ringo Leese
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using Titanium.Web.Proxy.Models;

namespace Titanium.Web.Proxy.Http2.Hpack;

public class DynamicTable
{
    // a circular queue of header fields
    private HttpHeader[] headerFields = Array.Empty<HttpHeader>();
    private int head;
    private int tail;

    /// <summary>
    ///     Return the maximum allowable size of the dynamic table.
    /// </summary>
    /// <value>
    ///     The capacity.
    /// </value>
    // ensure setCapacity creates the array
    public int Capacity { get; private set; } = -1;

    /// <summary>
    ///     Return the current size of the dynamic table.
    ///     This is the sum of the size of the entries.
    /// </summary>
    /// <value>
    ///     The size.
    /// </value>
    public int Size { get; private set; }

    /// <summary>
    ///     Creates a new dynamic table with the specified initial capacity.
    /// </summary>
    /// <param name="initialCapacity">Initial capacity.</param>
    public DynamicTable(int initialCapacity)
    {
        SetCapacity(initialCapacity);
    }

    /// <summary>
    ///     Return the number of header fields in the dynamic table.
    /// </summary>
    public int Length()
    {
        int length;
        if (head < tail)
            length = headerFields.Length - tail + head;
        else
            length = head - tail;

        return length;
    }

    /// <summary>
    ///     Return the header field at the given index.
    ///     The first and newest entry is always at index 1,
    ///     and the oldest entry is at the index length().
    /// </summary>
    /// <returns>The entry.</returns>
    /// <param name="index">Index.</param>
    public HttpHeader GetEntry(int index)
    {
        if (index <= 0 || index > Length()) throw new IndexOutOfRangeException();

        var i = head - index;
        if (i < 0) return headerFields[i + headerFields.Length]!;

        return headerFields[i]!;
    }

    /// <summary>
    ///     Add the header field to the dynamic table.
    ///     Entries are evicted from the dynamic table until the size of the table
    ///     and the new header field is less than or equal to the table's capacity.
    ///     If the size of the new entry is larger than the table's capacity,
    ///     the dynamic table will be cleared.
    /// </summary>
    /// <param name="header">Header.</param>
    public void Add(HttpHeader header)
    {
        var headerSize = header.Size;
        if (headerSize > Capacity)
        {
            Clear();
            return;
        }

        while (Size + headerSize > Capacity) Remove();

        headerFields[head++] = header;
        Size += header.Size;
        if (head == headerFields.Length) head = 0;
    }

    /// <summary>
    ///     Remove and return the oldest header field from the dynamic table.
    /// </summary>
    public HttpHeader? Remove()
    {
        var removed = headerFields[tail];
        if (removed == null) return null;

        Size -= removed.Size;
        headerFields[tail++] = null!;
        if (tail == headerFields.Length) tail = 0;

        return removed;
    }

    /// <summary>
    ///     Remove all entries from the dynamic table.
    /// </summary>
    public void Clear()
    {
        while (tail != head)
        {
            headerFields[tail++] = null!;
            if (tail == headerFields.Length) tail = 0;
        }

        head = 0;
        tail = 0;
        Size = 0;
    }

    /// <summary>
    ///     Set the maximum size of the dynamic table.
    ///     Entries are evicted from the dynamic table until the size of the table
    ///     is less than or equal to the maximum size.
    /// </summary>
    /// <param name="capacity">Capacity.</param>
    public void SetCapacity(int capacity)
    {
        if (capacity < 0) throw new ArgumentException("Illegal Capacity: " + capacity);

        // initially capacity will be -1 so init won't return here
        if (Capacity == capacity) return;

        Capacity = capacity;

        if (capacity == 0)
            Clear();
        else
            // initially size will be 0 so remove won't be called
            while (Size > capacity)
                Remove();

        var maxEntries = capacity / HttpHeader.HttpHeaderOverhead;
        if (capacity % HttpHeader.HttpHeaderOverhead != 0) maxEntries++;

        // check if capacity change requires us to reallocate the array
        if (headerFields != null && headerFields.Length == maxEntries) return;

        var tmp = new HttpHeader[maxEntries];

        // initially length will be 0 so there will be no copy
        var len = Length();
        var cursor = tail;
        for (var i = 0; i < len; i++)
        {
            var entry = headerFields![cursor++];
            tmp[i] = entry!;
            if (cursor == headerFields.Length) cursor = 0;
        }

        tail = 0;
        head = tail + len;
        headerFields = tmp;
    }
}